﻿package com.gyul.findmystock.analysis;

import java.util.ArrayList;
import java.util.Collections;

import com.gyul.findmystock.db.DaoQuote;
import com.gyul.findmystock.entity.AnalysisEntity;
import com.gyul.findmystock.util.Constants;

public class GeneticAlgorithm {
	static private final String TAG = "GeneticAlgorithm";
	
	//선택압 조정 상수
	static final double SELECTION_PRESSURE = 4;
	//유전자 최대 길이 
	static final int GENE_SIZE = 24;
	
	static public final int SHORT_SIZE = 7;
	static public final int SHORT_CHECK = 0x40;
	static public final int LONG_SIZE = 8;
	static public final int LONG_CHECK = 0x80;
	static public final int MACD_SIZE = 8;
	static public final int MACD_CHECK = 0x80;
		
	/* 
	 * 적합도를 측정하기 위한 이익 계산을 시작하는 INDEX
	 * MACD의 파라미터에 따라 데이터의 갯 수가 다르므로 
	 * 가장 큰 BegIdx.value가 나올때의 INDEX 를 START_INDEX로 하여
	 * START_INDEX 부터 이익을 계산한다.
	 */	
	public int startIndex = 750;
		
	int population;
	int maxGeneration;
	int generation;
	int geneX, geneY;
    
	double crossoverRate;
	double mutationRate;
	double sumFitness;
	
	int [] close;
	MACD [] threadMacd = null;
	final int ThreadCount = 2;
		 			
	ArrayList<Gene> genes;
	AnalysisEntity resultEntity;
	/**
	 * 해집단 수, 최대 진화, 교배확률, 변이 확률, 학습에 필요한 주가 리스트
	 * @param population
	 * @param maxGeneration
	 * @param crossoverRate
	 * @param mutationRate
	 * @param quoteList
	 */
	public GeneticAlgorithm(AnalysisEntity analysisEntity, int population, int maxGeneration, double crossoverRate, double mutationRate) {
		this.population = population;
		this.maxGeneration = maxGeneration;
		this.generation = 0;
		this.crossoverRate = crossoverRate;
		this.mutationRate = mutationRate;
		
		DaoQuote daoQuote = new DaoQuote(TAG, analysisEntity.symbol);		
		this.close = daoQuote.selectClose();
		
		daoQuote.release();
		
		this.startIndex = analysisEntity.startIndex;

		threadMacd = new MACD [ThreadCount];
		for ( int i = 0; i < ThreadCount; i++ ) {
			threadMacd[i] = new MACD(this.close);
		}

				
		genes = new ArrayList<Gene> ();
		resultEntity = new AnalysisEntity(analysisEntity);
	}
	
	public int getGeneration() {
		return generation;
	}
	
	public void initialize() {
//		Log.d(TAG, "initializePopulation");
		initializePopulation();
		
//		Log.d(TAG, "fitnessMeasure");
		fitnessMeasure();
	}
	
	public void run() {
		//해집단 수 * 교배 확률 만큼 교배를 반복한다.
		for ( int i = 0; i < population * crossoverRate; i++ ) {
			//두 유전자 선택
//			Log.d(TAG, "selection()");
			selection();
			//교배
			
//			Log.d(TAG, "crossover()");
			crossover();
		}
				
		//각각의 유전자에 대해 난수를 발생하여 변이 확률보다 적게 나왔을 때 변이한다.		
//		Log.d(TAG, "mutation()");
		mutation();
		
		//적합도 평가
//		Log.d(TAG, "fitnessMeasure()");
		fitnessMeasure();
		
		generation++;
	
	}	
	
	/**
	 * 초기 해집단 생성
	 * 2 <= Short MA <= 127
	 * 3 <= Long MA <= 255
	 * 2 <= Macd MA <= 255
	 * 위 범위 사이의 난수를 발생시키며 Long MA는 항상 Short MA 보다 크도록 한다.
	 */
	public void initializePopulation() {
		//총 해집단의 수 만큼 생성
		genes.clear();
		
		//이전 분석 결과 등록
		genes.add(new Gene(resultEntity.shortMA, resultEntity.longMA, resultEntity.macdMA));
		
		for ( int i = 1; i < population; i++ ) {
			int shortMA = getRandomNumber(2, 127);
			int longMA = getRandomNumber(shortMA+1, 255);			
			int macdMA = getRandomNumber(2, 255);
			genes.add(new Gene(shortMA, longMA, macdMA));
		}
	}
	
	/**
	 * 적함도 측정 함수
	 * 품질 비례 룰렛휠 선택 하기 위한 적합도 측정
	 * 
     * 적합도 = 이익[i] - 최소 이익 + (최대 이익 - 최소 이익)/(선택압 - 1)
     * 위 공식으로 적합도를 평가하여 최대 적합도가 최소 적합도의 K배가 되도록 한다. 
	 */
	public void fitnessMeasure() {	
//		Log.d(TAG, "calculateProfit()");
		calculateProfitThreads();
						
		//유전자들을 이익에 따라 내림차순으로 정렬한다.
		Collections.sort(genes);
		
		//최대, 최소 이익.
		double maxProfit = genes.get(0).profit;
		double minProfit = genes.get(genes.size()-1).profit;
		//선택압 조정
		double pressure = (maxProfit - minProfit)/(SELECTION_PRESSURE-1);
				
		//적합도의 총 합을 구한다.
		sumFitness = 0;
		for ( int i = 0; i < genes.size(); i++ ) {
			//적합도 = 이익[i] - 최소 이익 + (최대 이익 - 최소 이익)/(선택압 - 1)
			double fitness = genes.get(i).profit - minProfit + pressure;
			fitness = Double.parseDouble(String.format("%.3f", fitness));	
			sumFitness += fitness;
			genes.get(i).fitness = fitness;
//			Log.d(TAG, "Index : " + i + " : " + genes.get(i).profit  + " => " + fitness);			
		}
 	}	
	
		
	/**
	 * 선택 함수
	 * 
	 * 품질 비례 룰렛휠 선택 방식으로 선택하나 
	 * 최적해는 유지하기 위해 0(최적해)번째는 제외한다. 
	 */
	public void selection() {
		do {
			geneX = selectGene();
		} while ( geneX == 0 );
				
		do {
			geneY = selectGene();			

		} while ( geneY == geneX || geneY == 0 );
	}
	
	/**
	 * point = 0부터 적합도의 합까지의 난수
	 * 각 유전자의 적합도를 더하여 point보다 
	 * 크거나 같을 때의 Index를 선택한다.
	 * 
	 * 적합도가 클수록 넓은 범위를 차지 하기 때문에
	 * 좋은 유전자가 선택될 확률이 더 크다.
	 */
	public int selectGene() {
		int point = (int)(Math.random()*sumFitness);
		
		int sum = 0;
		for ( int i = 0; i < genes.size(); i++ ) {
			sum += genes.get(i).fitness;
			if ( point <= sum ) 
				return i;
		}
		return genes.size()-1;
	}
	
	/**
	 * 교배 함수
	 * 
	 * 유전자에 대해 변형 시킬 위치 to와 from을 난수 발생시켜 구한다.
	 * 선택된 두 유전자를 이진화 한 뒤 to ~ from 사이의 값을
	 * 0이면 1로 1이면 0으로 바꾼다.
	 */
	public void crossover() {
		//변형시킬 두 지점		
		int to = getRandomNumber(0, SHORT_SIZE-1);
		int from = getRandomNumber(to, SHORT_SIZE-1);
		
		Gene x = genes.get(geneX);
		Gene y = genes.get(geneY);
		
		x.shortMA = clossoverGene(to, from, SHORT_CHECK, x.shortMA);
		y.shortMA = clossoverGene(to, from, SHORT_CHECK, y.shortMA);
		
		to = getRandomNumber(0, LONG_SIZE-1);
		from = getRandomNumber(to, LONG_SIZE-1);		
		x.longMA = clossoverGene(to, from, LONG_CHECK, x.longMA);
		y.longMA = clossoverGene(to, from, LONG_CHECK, y.longMA);
		
		to = getRandomNumber(0, MACD_SIZE-1);
		from = getRandomNumber(to, MACD_SIZE-1);		
		x.macdMA = clossoverGene(to, from, MACD_CHECK, x.macdMA);
		y.macdMA = clossoverGene(to, from, MACD_CHECK, y.macdMA);
		
		x.checkGene();
		y.checkGene();
		
	}
	
	public int clossoverGene(int to, int from, int check, int value) {
		int size = 0;
		int result = 0;
		
		size = ( (check == SHORT_CHECK ) ? SHORT_SIZE : LONG_SIZE );
		
		for ( int i = 0; i < size; i++ ) {							
			if ( (value & check) == 0 ) {			
				if ( to <= i && i <= from ) {
					result += check;
				}
			} else {
				if ( !(to <= i && i <= from) ) {
					result += check;
				}		
			}
			
			check = check >> 1;
		}		
		return result;
	}
	
	/**
	 * 변이 함수
	 * 
	 * 모든 유전자들에 대해 난수를 발생시켜 변이 확률보다 작으면 
	 * 유전자를 변이 시킨다.
	 * 
	 * 유전자가 0이면 1로 1이면 0 으로 바꾸어 변이 시킨다.
	 */
	public void mutation() {
		for ( int i = 0; i < population; i++ ) {
			if ( Math.random() < mutationRate ) {				
				Gene gene = genes.get(i);
				gene.shortMA = mutationGene(SHORT_CHECK, gene.shortMA);
				gene.longMA = mutationGene(LONG_CHECK, gene.longMA);
				gene.macdMA = mutationGene(MACD_CHECK, gene.macdMA);
				gene.checkGene();
			}
		}		
	}
	
	public int mutationGene(int check, int value) {
		int size = 0;
		int result = 0;
		
		size = ( (check == SHORT_CHECK ) ? SHORT_SIZE : LONG_SIZE );
		
		for ( int i = 0; i < size; i++ ) {							
			if ( (value & check) == 0 ) {
				result += check;				
			}
		}		
		
		check = check >> 1;
		
		return result;
	}
	
	/**
	 * GA 종료 판단 함수
	 * @return
	 */
	public boolean isTerminate( ) {
		return maxGeneration <= generation;
	}
	
	public void terminate() {
		generation = maxGeneration + 1;
	}
	
	/**
	 * 이익을 계산한다.
	 * @param shortMA
	 * @param longMA
	 * @param macdMA
	 * @param close
	 * @return
	 */
//	private void calculateProfit(Gene gene) {
//		int profit = 0;
//	    int status;
//	    int tradeIndex = 0;
//	    int tradeClose = 0;
//	    
//	    macd.setDay(gene.shortMA, gene.longMA, gene.macdMA);
//	    macd.computeMacd();
//		
//		//초기 시작 자산 결정
//		status = Constants.READY;
//						
//		//모의 투자
//		for ( int i = startIndex+1; i < close.length; i++ ) {
//			//이전에 구매하였거나 팔았을 경우 상태 수정
//			if ( status == Constants.SELL )
//				status = Constants.READY;
//			else if ( status == Constants.BUY )
//				status = Constants.HOLD;
//			
//			
//			//Oscillator 매수  
//			if ( macd.osc[i-1] <= 0 && macd.osc[i] >= 0 && status == Constants.READY  ) {
//				status = Constants.BUY;
//				tradeIndex = i;
//				tradeClose = close[i];
//			} 
//			//Oscillator 매도 
//			else if ( macd.osc[i-1] >= 0 && macd.osc[i] <= 0 && status == Constants.HOLD ) {
//				status = Constants.SELL;
//				profit = PaperTrade.calculateSellProfit(profit, close[i], tradeClose);				
//				tradeIndex = i;
//				tradeClose = close[i]; //판매한 가격 저장
//			}						
//		}		
//		
//		gene.tradeIndex = tradeIndex;
//		gene.tradeClose = tradeClose;
//		gene.profit = profit;
//		gene.status = status;
//	}
	
	/**
	 * to부터 from 사이의 난수를 발생 시킨다.
	 */
	int getRandomNumber(int to, int from) {
		return (int)(Math.random()*(from+1-to)) + to;		
	}	
		
	public AnalysisEntity getResulet() {
		Gene resultGene = genes.get(0);
		resultEntity.shortMA = resultGene.shortMA;
		resultEntity.longMA = resultGene.longMA;
		resultEntity.macdMA = resultGene.macdMA;
		resultEntity.profit = resultGene.profit;
		resultEntity.tradeIndex = resultGene.tradeIndex;
		resultEntity.tradeClose = resultGene.tradeClose;
		resultEntity.status = resultGene.status;		
		resultEntity.updateTime = System.currentTimeMillis();
		resultEntity.validity = Constants.VALIDITY_ANALYSIS;
		return resultEntity;
	}
	
	public void calculateProfitThreads() {
		ProfitThread [] threads = new ProfitThread [4];
		int gap = population / ThreadCount;
		for ( int i = 0; i < ThreadCount; i++ ) {
			threads[i] = new ProfitThread(threadMacd[i], i * gap, (i+1) * gap);
			threads[i].start();
		}
		
		for ( int i = 0; i < ThreadCount; i++ ) {			
			try {
				threads[i].join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	public class ProfitThread extends Thread {
		MACD macd;
		int start;
		int end;
		
		public ProfitThread(MACD macd, int start, int end) {
			this.macd = macd;
			this.start = start;
			this.end = end;
		}
		
		@Override
		public void run() {
			Gene gene = null;
			for ( int index = start; index < end && isTerminate() == false; index++ ) {				
				int profit = 0;
			    int status;
			    int tradeIndex = 0;
			    int tradeClose = 0;
			    
			    gene = genes.get(index);
			    
			    macd.setDay(gene.shortMA, gene.longMA, gene.macdMA);
			    macd.computeMacd();
				
				//초기 시작 자산 결정
				status = Constants.READY;
								
				//모의 투자
				for ( int i = startIndex+1; i < close.length; i++ ) {
					//이전에 구매하였거나 팔았을 경우 상태 수정
					if ( status == Constants.SELL )
						status = Constants.READY;
					else if ( status == Constants.BUY )
						status = Constants.HOLD;
					
					
					//Oscillator 매수  
					if ( macd.osc[i-1] <= 0 && macd.osc[i] >= 0 && status == Constants.READY  ) {
						status = Constants.BUY;
						tradeIndex = i;
						tradeClose = close[i];
					} 
					//Oscillator 매도 
					else if ( macd.osc[i-1] >= 0 && macd.osc[i] <= 0 && status == Constants.HOLD ) {
						status = Constants.SELL;
						profit = PaperTrade.calculateSellProfit(profit, close[i], tradeClose);				
						tradeIndex = i;
						tradeClose = close[i]; //판매한 가격 저장
					}						
				}		
				
				gene.tradeIndex = tradeIndex;
				gene.tradeClose = tradeClose;
				gene.profit = profit;
				gene.status = status;
			}
		}
	}
}
